メンバーズのCURを元にコストを日単位で合算出力してみた

メンバーズのCURを元にコストを日単位で合算出力してみた

クラスメソッドメンバーズで提供しているCURを元に、日単位で利用日合算するシェルスクリプトを手掛けてみました。
Clock Icon2024.11.12

メンバーズが出力するCURは、AWS標準のCURと異なって時単位での出力制約があります。時単位出力自体は用途に応じて加工しやすく、半日や日単位、週単位などへの集計にも利用可能です。

しかし、問題点があります。それは、比較的大容量の複数圧縮ファイルを基に計算する必要があり、その手続を知る必要があるところです。その結果、日単位などでの合算需要が絶えません。社内でも、日単位合算出力に関する問い合わせが定期的に寄せられています。

S3上のCURをシェルで操作することは時折ありましたが、今回は複数ファイルを結合した上で日単位計算までスクリプトからカバーできないか試してみました。

手順

一回のコマンド実行で担保するのは難しかったため、段階毎に分けて実行していきます。

  1. 出力用の環境変数を設定する
  2. CURのヘッダ一覧を出力する
  3. 分割出力されているCURを一つのCSVとして結合する
  4. ヘッダ一覧とCSVを結合する
  5. 日単位でコストを合算する

direnv、awscli、csvqのインストールを事前に行っておきましょう。

出力用の環境変数を設定する

AWSCLI用の変数をdirenvで設定しておきます。

vim .envrc
export AWS_DEFAULT_PROFILE=self
export BUCKET_NAME=cm-cur-XXXXXXXXXXX
export TARGET_DATE=2024-11-12
direnv allow .

TARGET_DATEは実際に合算で用いるCURの出力日を指定します。これは出力最中のCURをうっかり掴んでしまわないための処置となります。

CURのヘッダ一覧を出力する

以下のコマンドを実行します。

aws s3 ls --profile self --recursive s3://${BUCKET_NAME}/ |
  grep cur_hourly-Manifest.json | 
  sort | 
  awk -v b="$BUCKET_NAME" -v d="$TARGET_DATE" '{ if($1 == d) { print "s3://" b "/" $4 }}' |
  tail -1 |
  xargs -I{} aws --profile self s3 cp {} - |
  jq -r ".reportKeys[]" | 
  head -1 | 
  xargs -n1 -I{} aws --profile self s3 cp s3://${BUCKET_NAME}/{} - | 
  gunzip | 
  sed -ne "1p" |
  tee - > data_0.csv

以下の手続きを行っています。CURの1行目がヘッダであることを利用しています。

  1. 指定バケットを再帰参照する
  2. cur_hourly-Manifest.jsonを含むパスのみに絞る
  3. 日付で並び替える
  4. 指定日に出力されたcur_hourly-Manifest.jsonに絞り込む
  5. 指定日の中でより新しいcur_hourly-Manifest.jsonを選択する
  6. cur_hourly-Manifest.jsonの内容をバッファに出力
  7. バッファ内からCURのキー群を取得
  8. キー群から一つを取得
  9. キーを元にCURの中身をバッファ出力
  10. バッファの中身を解凍する
  11. 最初の1行目を取得してCSVに出力

分割出力されているCURを一つのCSVとして結合する

以下のコマンドを実行します。圧縮されたCURの中身を全てバッファに出力するため、十二分にメモリを担保した環境で行ってください。

aws s3 ls --profile self --recursive s3://${BUCKET_NAME}/ |
  grep cur_hourly-Manifest.json | 
  sort | 
  awk -v b="$BUCKET_NAME" -v d="$TARGET_DATE" '{ if($1 == d) { print "s3://" b "/" $4 }}' |
  tail -1 |
  xargs -I{} aws --profile self s3 cp {} - |
  jq -r ".reportKeys[]" | (
  while read cur; do
    echo s3://${BUCKET_NAME}/$cur
    aws --profile self s3 cp s3://${BUCKET_NAME}/$cur - | 
    gunzip | 
    sed "1d" |
    tee - >> data_1.csv
  done )

以下の手続きを行っています。ベースはCURのヘッダ取得と変わりません。

  1. 指定バケットを再帰参照する
  2. cur_hourly-Manifest.jsonを含むパスのみに絞る
  3. 日付で並び替える
  4. 指定日に出力されたcur_hourly-Manifest.jsonに絞り込む
  5. 指定日の中でより新しいcur_hourly-Manifest.jsonを選択する
  6. cur_hourly-Manifest.jsonの内容をバッファに出力
  7. バッファ内からCURのキー群を取得
  8. キーを元にCURの中身をバッファ出力
  9. バッファの中身を解凍する
  10. ヘッダ行を削除
  11. CSVに追加出力

ヘッダ一覧とCSVを結合する

csvqでの処理用に結合します。

cat data_1.csv >> data_0.csv

日単位でコストを合算する

csvqで処理します。十分なメモリを確保した環境で実行してください。処理結果として、時単位での合算結果「daily_grep.csv」と日単位での合算結果「sum_by_daily.csv」が出力されます。

csvq 'create table `daily_grep.csv` (lineitem_usagestartdate, date, sum_cost) as select `lineItem/UsageStartDate`, REGEXP_FIND(`lineItem/UsageStartDate`, "^(\d{4}-\d{2}-\d{2})"), sum(`lineItem/UnblendedCost`) from `data_0.csv` group by `lineItem/UsageStartDate` order by `lineItem/UsageStartDate`;
create table `sum_by_daily.csv` (date, cost) as select `date`, sum(`sum_cost`) as cost from `daily_grep.csv` group by `date` order by `date`;'

あとがき

以下は記事中のコマンドを一つのシェルスクリプトにまとめたものとなります。基本的に一回の実行で目的の合算されたCSVファイルを生成できるはずです。

aws s3 ls --profile self --recursive s3://${BUCKET_NAME}/ |
  grep cur_hourly-Manifest.json | 
  sort | 
  awk -v b="$BUCKET_NAME" -v d="$TARGET_DATE" '{ if($1 == d) { print "s3://" b "/" $4 }}' |
  tail -1 |
  xargs -I{} aws --profile self s3 cp {} - |
  jq -r ".reportKeys[]" | 
  head -1 | 
  xargs -n1 -I{} aws --profile self s3 cp s3://${BUCKET_NAME}/{} - | 
  gunzip | 
  sed -ne "1p" |
  tee - > data_0.csv
aws s3 ls --profile self --recursive s3://${BUCKET_NAME}/ |
  grep cur_hourly-Manifest.json | 
  sort | 
  awk -v b="$BUCKET_NAME" -v d="$TARGET_DATE" '{ if($1 == d) { print "s3://" b "/" $4 }}' |
  tail -1 |
  xargs -I{} aws --profile self s3 cp {} - |
  jq -r ".reportKeys[]" | (
  while read cur; do
    echo s3://${BUCKET_NAME}/$cur
    aws --profile self s3 cp s3://${BUCKET_NAME}/$cur - | 
    gunzip | 
    sed "1d" |
    tee - >> data_1.csv
  done )
cat data_1.csv >> data_0.csv
csvq 'create table `daily_grep.csv` (lineitem_usagestartdate, date, sum_cost) as select `lineItem/UsageStartDate`, REGEXP_FIND(`lineItem/UsageStartDate`, "^(\d{4}-\d{2}-\d{2})"), sum(`lineItem/UnblendedCost`) from `data_0.csv` group by `lineItem/UsageStartDate` order by `lineItem/UsageStartDate`;
create table `sum_by_daily.csv` (date, cost) as select `date`, sum(`sum_cost`) as cost from `daily_grep.csv` group by `date` order by `date`;'

合算に条件を追加したい場合は、csvqに渡すSQL文を編集してください。CUR内のフィールドはそのまま保持されているため、フィールドに基づいた条件指定が可能であれば、柔軟に加工できるはずです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.